/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.std.memory;

import java.awt.Color;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import com.cburch.logisim.circuit.CircuitState;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeEvent;
import com.cburch.logisim.data.AttributeListener;
import com.cburch.logisim.data.AttributeOption;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.AttributeSets;
import com.cburch.logisim.data.Attributes;
import com.cburch.logisim.data.BitWidth;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.data.Value;
import com.cburch.logisim.gui.hex.HexFrame;
import com.cburch.logisim.instance.Instance;
import com.cburch.logisim.instance.InstanceData;
import com.cburch.logisim.instance.InstanceLogger;
import com.cburch.logisim.instance.InstancePainter;
import com.cburch.logisim.instance.InstanceState;
import com.cburch.logisim.instance.Port;
import com.cburch.logisim.instance.StdAttr;
import com.cburch.logisim.proj.Project;

public class Ram extends Mem {
	public static class Logger extends InstanceLogger {
		@Override
		public String getLogName(InstanceState state, Object option) {
			if (option instanceof Integer) {
				String disp = Strings.get("ramComponent");
				Location loc = state.getInstance().getLocation();
				return disp + loc + "[" + option + "]";
			} else {
				return null;
			}
		}

		@Override
		public Object[] getLogOptions(InstanceState state) {
			int addrBits = state.getAttributeValue(ADDR_ATTR).getWidth();
			if (addrBits >= logOptions.length)
				addrBits = logOptions.length - 1;
			synchronized (logOptions) {
				Object[] ret = logOptions[addrBits];
				if (ret == null) {
					ret = new Object[1 << addrBits];
					logOptions[addrBits] = ret;
					for (int i = 0; i < ret.length; i++) {
						ret[i] = Integer.valueOf(i);
					}
				}
				return ret;
			}
		}

		@Override
		public Value getLogValue(InstanceState state, Object option) {
			if (option instanceof Integer) {
				MemState s = (MemState) state.getData();
				int addr = ((Integer) option).intValue();
				return Value.createKnown(BitWidth.create(s.getDataBits()), s.getContents().get(addr));
			} else {
				return Value.NIL;
			}
		}
	}

	private static class RamState extends MemState implements InstanceData, AttributeListener {
		private Instance parent;
		private MemListener listener;
		private HexFrame hexFrame = null;
		private ClockState clockState;

		RamState(Instance parent, MemContents contents, MemListener listener) {
			super(contents);
			this.parent = parent;
			this.listener = listener;
			this.clockState = new ClockState();
			if (parent != null)
				parent.getAttributeSet().addAttributeListener(this);
			contents.addHexModelListener(listener);
		}

		@Override
		public void attributeListChanged(AttributeEvent e) {
		}

		@Override
		public void attributeValueChanged(AttributeEvent e) {
			AttributeSet attrs = e.getSource();
			BitWidth addrBits = attrs.getValue(Mem.ADDR_ATTR);
			BitWidth dataBits = attrs.getValue(Mem.DATA_ATTR);
			getContents().setDimensions(addrBits.getWidth(), dataBits.getWidth());
		}

		@Override
		public RamState clone() {
			RamState ret = (RamState) super.clone();
			ret.parent = null;
			ret.clockState = this.clockState.clone();
			ret.getContents().addHexModelListener(listener);
			return ret;
		}

		// Retrieves a HexFrame for editing within a separate window
		public HexFrame getHexFrame(Project proj) {
			if (hexFrame == null) {
				hexFrame = new HexFrame(proj, getContents());
				hexFrame.addWindowListener(new WindowAdapter() {
					@Override
					public void windowClosed(WindowEvent e) {
						hexFrame = null;
					}
				});
			}
			return hexFrame;
		}

		//
		// methods for accessing the write-enable data
		//
		public boolean setClock(Value newClock, Object trigger) {
			return clockState.updateClock(newClock, trigger);
		}

		void setRam(Instance value) {
			if (parent == value)
				return;
			if (parent != null)
				parent.getAttributeSet().removeAttributeListener(this);
			parent = value;
			if (value != null)
				value.getAttributeSet().addAttributeListener(this);
		}
	}

	static final AttributeOption BUS_COMBINED = new AttributeOption("combined", Strings.getter("ramBusSynchCombined"));

	static final AttributeOption BUS_ASYNCH = new AttributeOption("asynch", Strings.getter("ramBusAsynchCombined"));

	static final AttributeOption BUS_SEPARATE = new AttributeOption("separate", Strings.getter("ramBusSeparate"));
	static final Attribute<AttributeOption> ATTR_BUS = Attributes.forOption("bus", Strings.getter("ramBusAttr"),
			new AttributeOption[] { BUS_COMBINED, BUS_ASYNCH, BUS_SEPARATE });

	private static Attribute<?>[] ATTRIBUTES = { Mem.ADDR_ATTR, Mem.DATA_ATTR, StdAttr.LABEL, StdAttr.LABEL_FONT,
			StdAttr.ATTR_LABEL_COLOR, ATTR_BUS, Mem.ATTR_SELECTION };
	private static Object[] DEFAULTS = { BitWidth.create(8), BitWidth.create(8), "", StdAttr.DEFAULT_LABEL_FONT,
			Color.BLACK, BUS_COMBINED, Mem.SEL_LOW };
	private static final int OE = MEM_INPUTS + 0;
	private static final int CLR = MEM_INPUTS + 1;
	private static final int CLK = MEM_INPUTS + 2;

	private static final int WE = MEM_INPUTS + 3;

	private static final int DIN = MEM_INPUTS + 4;

	private static Object[][] logOptions = new Object[9][];

	public Ram() {
		super("RAM", Strings.getter("ramComponent"), 3);
		setIconName("ram.gif");
		setInstanceLogger(Logger.class);
	}

	@Override
	protected void configureNewInstance(Instance instance) {
		super.configureNewInstance(instance);
		instance.addAttributeListener();
	}

	@Override
	void configurePorts(Instance instance) {
		Object bus = instance.getAttributeValue(ATTR_BUS);
		if (bus == null)
			bus = BUS_COMBINED;
		boolean asynch = bus == null ? false : bus.equals(BUS_ASYNCH);
		boolean separate = bus == null ? false : bus.equals(BUS_SEPARATE);

		int portCount = MEM_INPUTS;
		if (asynch)
			portCount += 2;
		else if (separate)
			portCount += 5;
		else
			portCount += 3;
		Port[] ps = new Port[portCount];
		configureStandardPorts(instance, ps);
		if (instance.getAttributeValue(Mem.ATTR_SELECTION) == Mem.SEL_HIGH)
			ps[CS].setToolTip(Strings.getter("memCSTip", "0"));
		else
			ps[CS].setToolTip(Strings.getter("memCSTip", "1"));
		ps[OE] = new Port(-50, 40, Port.INPUT, 1);
		ps[OE].setToolTip(Strings.getter("ramOETip"));
		ps[CLR] = new Port(-30, 40, Port.INPUT, 1);
		ps[CLR].setToolTip(Strings.getter("ramClrTip"));
		if (!asynch) {
			ps[CLK] = new Port(-70, 40, Port.INPUT, 1);
			ps[CLK].setToolTip(Strings.getter("ramClkTip"));
		}
		if (separate) {
			ps[WE] = new Port(-110, 40, Port.INPUT, 1);
			ps[WE].setToolTip(Strings.getter("ramWETip"));
			ps[DIN] = new Port(-140, 20, Port.INPUT, DATA_ATTR);
			ps[DIN].setToolTip(Strings.getter("ramInTip"));
		} else {
			ps[DATA].setToolTip(Strings.getter("ramBusTip"));
			ps[DATA] = new Port(0, 0, Port.INOUT, DATA_ATTR);
		}
		instance.setPorts(ps);
	}

	@Override
	public AttributeSet createAttributeSet() {
		return AttributeSets.fixedSet(ATTRIBUTES, DEFAULTS);
	}

	@Override
	HexFrame getHexFrame(Project proj, Instance instance, CircuitState circState) {
		RamState state = (RamState) getState(instance, circState);
		return state.getHexFrame(proj);
	}

	@Override
	MemState getState(Instance instance, CircuitState state) {
		BitWidth addrBits = instance.getAttributeValue(ADDR_ATTR);
		BitWidth dataBits = instance.getAttributeValue(DATA_ATTR);

		RamState myState = (RamState) instance.getData(state);
		if (myState == null) {
			MemContents contents = MemContents.create(addrBits.getWidth(), dataBits.getWidth());
			myState = new RamState(instance, contents, new MemListener(instance));
			instance.setData(state, myState);
		} else {
			myState.setRam(instance);
		}
		return myState;
	}

	@Override
	MemState getState(InstanceState state) {
		BitWidth addrBits = state.getAttributeValue(ADDR_ATTR);
		BitWidth dataBits = state.getAttributeValue(DATA_ATTR);

		RamState myState = (RamState) state.getData();
		if (myState == null) {
			MemContents contents = MemContents.create(addrBits.getWidth(), dataBits.getWidth());
			Instance instance = state.getInstance();
			myState = new RamState(instance, contents, new MemListener(instance));
			state.setData(myState);
		} else {
			myState.setRam(state.getInstance());
		}
		return myState;
	}

	@Override
	protected void instanceAttributeChanged(Instance instance, Attribute<?> attr) {
		super.instanceAttributeChanged(instance, attr);
		if (attr == StdAttr.LABEL || attr == StdAttr.LABEL_FONT || attr == StdAttr.ATTR_LABEL_COLOR)
			return;
		configurePorts(instance);
		instance.fireInvalidated();
	}

	@Override
	public void paintInstance(InstancePainter painter) {
		super.paintInstance(painter);
		Object busVal = painter.getAttributeValue(ATTR_BUS);
		boolean asynch = busVal == null ? false : busVal.equals(BUS_ASYNCH);
		boolean separate = busVal == null ? false : busVal.equals(BUS_SEPARATE);

		if (!asynch)
			painter.drawClock(CLK, Direction.NORTH);
		painter.drawPort(OE, Strings.get("ramOELabel"), Direction.SOUTH);
		painter.drawPort(CLR, Strings.get("ramClrLabel"), Direction.SOUTH);

		if (separate) {
			painter.drawPort(WE, Strings.get("ramWELabel"), Direction.SOUTH);
			painter.getGraphics().setColor(Color.BLACK);
			painter.drawPort(DIN, Strings.get("ramDataLabel"), Direction.EAST);
		}
	}

	@Override
	public void propagate(InstanceState state) {
		RamState myState = (RamState) getState(state);
		BitWidth dataBits = state.getAttributeValue(DATA_ATTR);
		Object busVal = state.getAttributeValue(ATTR_BUS);
		boolean asynch = busVal == null ? false : busVal.equals(BUS_ASYNCH);
		boolean separate = busVal == null ? false : busVal.equals(BUS_SEPARATE);

		Value addrValue = state.getPort(ADDR);
		boolean selection = state.getAttributeValue(Mem.ATTR_SELECTION) == Mem.SEL_HIGH;
		boolean chipSelect = !(state.getPort(CS) == Value.FALSE && selection
				|| state.getPort(CS) == Value.TRUE && !selection);
		boolean triggered = asynch || myState.setClock(state.getPort(CLK), StdAttr.TRIG_RISING);
		boolean outputEnabled = state.getPort(OE) != Value.FALSE;
		boolean shouldClear = state.getPort(CLR) == Value.TRUE;

		if (shouldClear) {
			myState.getContents().clear();
		}

		if (!chipSelect) {
			myState.setCurrent(-1);
			state.setPort(DATA, Value.createUnknown(dataBits), DELAY);
			return;
		}

		int addr = addrValue.toIntValue();
		if (!addrValue.isFullyDefined() || addr < 0)
			return;
		if (addr != myState.getCurrent()) {
			myState.setCurrent(addr);
			myState.scrollToShow(addr);
		}

		if (!shouldClear && triggered) {
			boolean shouldStore;
			if (separate) {
				shouldStore = state.getPort(WE) != Value.FALSE;
			} else {
				shouldStore = !outputEnabled;
			}
			if (shouldStore) {
				Value dataValue = state.getPort(separate ? DIN : DATA);
				myState.getContents().set(addr, dataValue.toIntValue());
			}
		}

		if (outputEnabled) {
			int val = myState.getContents().get(addr);
			state.setPort(DATA, Value.createKnown(dataBits, val), DELAY);
		} else {
			state.setPort(DATA, Value.createUnknown(dataBits), DELAY);
		}
	}
}
